/*******************************************************************************
 * Copyright (c) 2010, 2011 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.ui.viewmodel.properties;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.dsf.concurrent.DsfMultiStatus;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.core.runtime.IStatus;

/**
 * Status object for use with the IPropertiesUpdate.  This status class
 * allows setting a different status result for each property.  This allows
 * for better interpretation of status by the client of the update.
 * <p>
 * This status class derives from MultiStatus class such that the status 
 * objects for each property can be accessed through the standard 
 * {@link #getChildren()} method.  Also, multiple properties can reference 
 * the same status object, meaning that the number of properties returned
 * by {@link #getProperties()} may be greater than the status objects 
 * returned by <code>getChildren()</code>.
 * <p>
 * The properties status object does not have its own message, severity, 
 * error status or exception.  All these attributes are calculated from 
 * the child status objects.  If the status has more than one status child, 
 * the String returned by {@link #getMessage()} is: "Multiple errors reported".
 * 
 * @since 2.2
 */
public class PropertiesUpdateStatus extends DsfMultiStatus {

    final private Map<String,IStatus> fPropertiesStatus = new HashMap<String, IStatus>(1);
    private boolean fFirstStatusSet;
    
    public PropertiesUpdateStatus() {
        super(DsfUIPlugin.PLUGIN_ID, 0, "", null); //$NON-NLS-1$
    }
    
    /**
     * Returns set of properties that have an additional status specified.
     */
    public Set<String> getProperties() {
        return fPropertiesStatus.keySet();
    }
    
    /**
     * Returns an additional status for the given property in a property 
     * update.  Returned value may be <code>null</code> if no additional
     * status is given.
     */
    public IStatus getStatus(String property) {
        return fPropertiesStatus.get(property);
    }

    /**
     * Sets the given status for the given property.
     */
    public void setStatus(String property, IStatus status) {
        IStatus child = findEquivalentChild(status);
        if (child != null) {
            status = child;
        } else {
            add(status);
        }

        fPropertiesStatus.put(property, status);
    }

    /**
     * Sets the given status for the properties array.
     */
    public void setStatus(String[] properties, IStatus status) {
        IStatus child = findEquivalentChild(status);
        if (child != null) {
            status = child;
        } else {
            add(status);
        }

        for (String property : properties) {
            fPropertiesStatus.put(property, status);
        }
    }

    /**
     * Merges data in the new status into the base status data, and returns the 
     * resulting status. Only properties specified in the given set are merged.
     * <p>
     * The new status is considered to be more up to date than the base 
     * status and its data overrides the base status .  If the base status 
     * holds an error for a given property, which is found in the 
     * given set, and the new status does not, then the base error status is 
     * removed. 
     *
     * @param baseStatus Properties into which the new status properties will 
     * be merged. 
     * @param newStatus Properties status to merge. 
     * @param properties The properties to consider in the new status.
     * @return Resulting merged status object. 
     */
    public static PropertiesUpdateStatus mergePropertiesStatus(PropertiesUpdateStatus baseStatus, 
        PropertiesUpdateStatus newStatus, Set<String> properties) 
    {
        PropertiesUpdateStatus mergedStatus = new PropertiesUpdateStatus();
        // Copy the property status map from the base status.
        mergedStatus.fPropertiesStatus.putAll(baseStatus.fPropertiesStatus);
        
        // Add in the property statuses from the new status, but only for the 
        // specified properties.
        for (String property : properties) {
            IStatus propertyStatus = newStatus.getStatus(property);
            if (propertyStatus != null) {
                mergedStatus.fPropertiesStatus.put(property, propertyStatus);
            } else {
                mergedStatus.fPropertiesStatus.remove(property);
            }
        }

        // Children of merged status should contain all statuses that are found in the fPropertiesStatus map, but 
        // without duplicates.
        Set<IStatus> children = new HashSet<IStatus>((baseStatus.getChildren().length + newStatus.getChildren().length) * 4/3);
        children.addAll(mergedStatus.fPropertiesStatus.values());
        for (IStatus child : children) {
            mergedStatus.add(child);
        }

        // Merged status should contain all children statuses that were added without a corresponding property to the 
        // base status and to the new status. 
        Collection<IStatus> baseStatusPropertyChildren = baseStatus.fPropertiesStatus.values();
        for (IStatus baseStatusChild : baseStatus.getChildren()) {
            if (!baseStatusPropertyChildren.contains(baseStatusChild)) {
                mergedStatus.add(baseStatusChild);
            }
        }
        Collection<IStatus> newStatusPropertyChildren = newStatus.fPropertiesStatus.values();
        for (IStatus newStatusChild : newStatus.getChildren()) {
            if (!newStatusPropertyChildren.contains(newStatusChild)) {
                mergedStatus.add(newStatusChild);
            }
        }
        
        return mergedStatus;
    }
    
    /**
     * Adds the given status object as a child of this status.  If there's an
     * equivalent child status already, the new status is ignored.
     */
    @Override
    public void add(IStatus status) {
        if (findEquivalentChild(status) != null) {
            return;
        }
        
        super.add(status);

        boolean firstSet;
        synchronized(this) {
            firstSet = fFirstStatusSet;
            fFirstStatusSet = true;
        }
        
        if (!firstSet) {
            setMessage(status.getMessage());
        } else {
            setMessage(MessagesForProperties.PropertiesUpdateStatus_message);
        }        
    }
    
    /**
     * Finds a child status that is equivalent to the given status.
     */
    private IStatus findEquivalentChild(IStatus status) {
        if (getChildren().length != 0) {
            for (IStatus child : getChildren()) {
                if (areEquivalent(child, status)) {
                    return child;
                }
            }
        }
        return null;
    }    
    
    /**
     * Compares two status objects to determine if they are equivalent.
     */
    private boolean areEquivalent(IStatus s1, IStatus s2) {
        if ( (s1 == null && s2 != null) || (s1 != null && s2 == null) ) 
        {
            return false;
        }
        if (s1 == null) {
            return true;
        }
        if ( (s1.getSeverity() != s2.getSeverity()) ||
             !s1.getPlugin().equals(s2.getPlugin()) ||
             (s1.getCode() != s2.getCode()) )
        {
            return false;
        }
        if ( (s1.getException() == null && s1.getException() != null) ||
             (s1.getException() != null && s1.getException() == null) ||
             (s1.getException() != null && !s1.getException().equals(s2.getException())) )
        {
            return false;
        }
        return s1.getMessage().equals(s2.getMessage());
    };

    /**
     * Convenience method that returns and optionally creates a properties 
     * update status object for the given update.
     */
    public static PropertiesUpdateStatus getPropertiesStatus(IPropertiesUpdate update) {
        IStatus updateStatus = update.getStatus();
        if (updateStatus instanceof PropertiesUpdateStatus) {
            return (PropertiesUpdateStatus)updateStatus; 
        } else {
            PropertiesUpdateStatus propertiesStatus = new PropertiesUpdateStatus(); 
            update.setStatus(propertiesStatus);
            if (!updateStatus.isOK()) {
                propertiesStatus.add(updateStatus);
            }
            return propertiesStatus;
        }
    }
    
    /**
     * Convenience method that returns and optionally creates a properties 
     * update status object for the given update.
     */
    public static PropertiesUpdateStatus makePropertiesStatus(IStatus updateStatus) {
        if (updateStatus instanceof PropertiesUpdateStatus) {
            return (PropertiesUpdateStatus)updateStatus; 
        } else {
            PropertiesUpdateStatus propertiesStatus = new PropertiesUpdateStatus(); 
            if (!updateStatus.isOK()) {
                propertiesStatus.add(updateStatus);
            }
            return propertiesStatus;
        }
    }
}
